'use strict';

var fs = require('fs'),
  union = require('union'),
  httpServerCore = require('./core'),
  auth = require('basic-auth'),
  httpProxy = require('http-proxy'),
  corser = require('corser'),
  secureCompare = require('secure-compare');

//
// Remark: backwards compatibility for previous
// case convention of HTTP
//
exports.HttpServer = exports.HTTPServer = HttpServer;

/**
 * Returns a new instance of HttpServer with the
 * specified `options`.
 */
exports.createServer = function (options) {
  return new HttpServer(options);
};

/**
 * Constructor function for the HttpServer object
 * which is responsible for serving static files along
 * with other HTTP-related features.
 */
function HttpServer(options) {
  options = options || {};
  var proxyAll = options.proxyAll === true || options.proxyAll === 'true';

  if (proxyAll && typeof options.proxy !== 'string') {
    throw new Error('proxyAll option requires "proxy" to be configured');
  }

  if (options.root) {
    this.root = options.root;
  } else {
    try {
      // eslint-disable-next-line no-sync
      fs.lstatSync('./public');
      this.root = './public';
    } catch (err) {
      this.root = './';
    }
  }

  // CRLF injection prevention
  for ( const [key, value] of Object.entries(options.headers || {}) ) {
    if (typeof key !== 'string' || typeof value !== 'string') {
      throw new Error('Header is not a string or contains CRLF');
    }
    if (key.includes('\r') || key.includes('\n') || value.includes('\r') || value.includes('\n')) {
      throw new Error('Header is not a string or contains CRLF');
    }
  }

  this.headers = options.headers || {};
  this.headers['Accept-Ranges'] = 'bytes';

  this.cache = (
    // eslint-disable-next-line no-nested-ternary
    options.cache === undefined ? 3600 :
    // -1 is a special case to turn off caching.
    // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#Preventing_caching
      options.cache === -1 ? 'no-cache, no-store, must-revalidate' :
        options.cache // in seconds.
  );
  this.showDir = options.showDir !== 'false';
  this.dirOverrides404 = options.dirOverrides404;
  this.autoIndex = options.autoIndex !== 'false';
  this.showDotfiles = options.showDotfiles;
  this.gzip = options.gzip === true;
  this.brotli = options.brotli === true;
  this.forceContentEncoding = options.forceContentEncoding === true;
  if (options.ext) {
    this.ext = options.ext === true
      ? 'html'
      : options.ext;
  }
  this.contentType = options.contentType ||
    (this.ext === 'html' ? 'text/html' : 'application/octet-stream');

  var before = options.before ? options.before.slice() : [];

  if (options.logFn) {
    before.push(function (req, res) {
      options.logFn(req, res);
      res.emit('next');
    });
  }

  if (options.username || options.password) {
    if (!options.username || !options.password) {
      throw new Error('Basic authentication requires both username and password to be specified');
    }

    before.push(function (req, res) {
      var credentials = auth(req);

      // We perform these outside the if to avoid short-circuiting and giving
      // an attacker knowledge of whether the username is correct via a timing
      // attack.
      if (credentials) {
        // if credentials is defined, name and pass are guaranteed to be string
        // type
        var usernameEqual = secureCompare(options.username.toString(), credentials.name);
        var passwordEqual = secureCompare(options.password.toString(), credentials.pass);
        if (usernameEqual && passwordEqual) {
          return res.emit('next');
        }
      }

      res.statusCode = 401;
      res.setHeader('WWW-Authenticate', 'Basic realm=""');
      res.end('Access denied');
    });
  }

  if (options.coop) {
    this.headers['Cross-Origin-Opener-Policy'] = options.coopHeader || 'same-origin';
    this.headers['Cross-Origin-Embedder-Policy'] = 'require-corp';
  }

  // CORS configuration:
  // --cors enables CORS by setting Access-Control-Allow-Origin to '*'
  // --cors=header1,header2 also adds custom headers to Access-Control-Allow-Headers
  if (options.cors) {
    this.headers['Access-Control-Allow-Origin'] = '*';
    this.headers['Access-Control-Allow-Headers'] = 'Origin, X-Requested-With, Content-Type, Accept, Range';
    if (options.corsHeaders) {
      options.corsHeaders.split(/\s*,\s*/)
        .forEach(function (h) { this.headers['Access-Control-Allow-Headers'] += ', ' + h; }, this);
    }
    before.push(corser.create(options.corsHeaders ? {
      requestHeaders: this.headers['Access-Control-Allow-Headers'].split(/\s*,\s*/)
    } : null));
  }

  if (options.robots) {
    before.push(function (req, res) {
      if (req.url === '/robots.txt') {
        res.setHeader('Content-Type', 'text/plain');
        var robots = options.robots === true
          ? 'User-agent: *\nDisallow: /'
          : options.robots.replace(/\\n/, '\n');

        return res.end(robots);
      }

      res.emit('next');
    });
  }

  if (!proxyAll) {
    before.push(httpServerCore({
      root: this.root,
      baseDir: options.baseDir,
      cache: this.cache,
      showDir: this.showDir,
      showDotfiles: this.showDotfiles,
      autoIndex: this.autoIndex,
      defaultExt: this.ext,
      dirOverrides404: this.dirOverrides404,
      gzip: this.gzip,
      brotli: this.brotli,
      forceContentEncoding: this.forceContentEncoding,
      contentType: this.contentType,
      mimetypes: options.mimetypes,
      handleError: typeof options.proxy !== 'string'
    }));
  }

  if (typeof options.proxy === 'string') {
    var proxyOptions = options.proxyOptions || {};
    var proxy = httpProxy.createProxyServer({
      ...proxyOptions,
      target: options.proxy,
      changeOrigin: true,
    });
    before.push(function (req, res) {
      proxy.web(req, res, {}, function (err, req, res) {
        if (options.logFn) {
          options.logFn(req, res, {
            message: err.message,
            status: res.statusCode });
        }
        res.emit('next');
      });
    });
  }

  var serverOptions = {
    before: before,
    headers: this.headers,
    onError: function (err, req, res) {
      if (options.logFn) {
        options.logFn(req, res, err);
      }

      res.end();
    }
  };

  if (options.https) {
    serverOptions.https = options.https;
  }

  this.server = serverOptions.https && serverOptions.https.passphrase
    // if passphrase is set, shim must be used as union does not support
    ? require('./shims/https-server-shim')(serverOptions)
    : union.createServer(serverOptions);

  if (isNaN(options.timeout) || isNaN(parseFloat(options.timeout))) {
    this.server.setTimeout(120);
  } else {
    // set custom timeout only if options.timeout is a numeric string
    this.server.setTimeout(Math.max(0, Number(options.timeout)));
  }

  if (typeof options.proxy === 'string' && options.websocket) {
    this.server.on('upgrade', function (request, socket, head) {
      proxy.ws(request, socket, head, {
        target: options.proxy,
        changeOrigin: true
      }, function (err, req, res) {
        if (options.logFn) {
          options.logFn(req, res, {
            message: err?.message,
            status: res?.statusCode });
        }
        res.emit('next');
      });
    });
  }
}

HttpServer.prototype.listen = function () {
  this.server.listen.apply(this.server, arguments);
};

HttpServer.prototype.close = function () {
  return this.server.close();
};

HttpServer.prototype.address = function () {
  return this.server.address();
};
